
#ifndef ROPUFU_MULTI_SENSOR_STATISTIC_BAYES_CUSUM_HPP_INCLUDED
#define ROPUFU_MULTI_SENSOR_STATISTIC_BAYES_CUSUM_HPP_INCLUDED

#include "../information/information_at_most.hpp"
#include "../information/information_exact.hpp"
#include "../information/information_none.hpp"
#include "../more_math.hpp"
#include "../window/window_all_cusum.hpp"
#include "../window/window_none.hpp"

#include <array>
#include <cmath>
#include <cstdint>
#include <ostream>
#include <string>
#include <type_traits>
#include <vector>

namespace ropufu
{
    namespace statistic
    {
        namespace detail
        {
            template <typename t_information_type>
            struct bayes_cusum_prior
            {
                static const std::size_t numerator = 1;
                static const std::size_t denominator = 2;

                static constexpr double value()
                {
                    return static_cast<double>(numerator) / static_cast<double>(denominator);
                }
            };

            template <std::size_t t_count_streams, std::size_t t_count_affected>
            struct bayes_cusum_prior<information_exact<t_count_streams, t_count_affected>>
            {
                static const std::size_t numerator = t_count_affected;
                static const std::size_t denominator = t_count_streams;

                static constexpr double value()
                {
                    return static_cast<double>(numerator) / static_cast<double>(denominator);
                }
            };

            template <std::size_t t_count_streams, std::size_t t_count_affected>
            struct bayes_cusum_prior<information_at_most<t_count_streams, t_count_affected>>
            {
                static const std::size_t numerator = t_count_affected;
                static const std::size_t denominator = 2 * t_count_streams;

                static constexpr double value()
                {
                    return static_cast<double>(numerator) / static_cast<double>(denominator);
                }
            };

            template <typename t_window_type>
            struct bayes_cusum_default_window
            {
                static t_window_type make()
                {
                    return t_window_type();
                }
            };
        }

        // WARNING: Only implemented for independent streams.
        template <std::size_t t_count_streams, typename t_information_type = void, typename t_window_type = window_all_cusum<t_count_streams>>
        struct bayes_cusum
        {
            static const std::size_t count_streams = t_count_streams;
            typedef typename switch_void<t_information_type, information_none<count_streams>>::type information_type;
            typedef typename switch_void<t_window_type, window_none<count_streams>>::type window_type;
            typedef bayes_cusum<count_streams, t_information_type, t_window_type> type;

            typedef detail::bayes_cusum_prior<information_type> information_helper_type;

        private:
            information_type m_prior_knowledge;
            window_type m_window;
            double m_current_statistic = 0.0;
            double m_bayes_prior = 0.0;
            bool m_has_reset = false;

            bayes_cusum() = delete;

            static double execute(double x, double prior)
            {
                return 1.0 - prior + prior * std::exp(x);
            }

            static double fuse(std::array<double, t_count_streams>& local_stat)
            {
                double statistic = 1.0;
                for (std::size_t k = 0; k < t_count_streams; k++) statistic *= local_stat[k];
                return statistic;
            }

        public:
            explicit bayes_cusum(const information_type& prior_knowledge)
                : m_prior_knowledge(prior_knowledge), m_window(detail::bayes_cusum_default_window<window_type>::make()), m_bayes_prior(information_helper_type::value())
            {
            }

            explicit bayes_cusum(const information_type& prior_knowledge, const window_type& window)
                : m_prior_knowledge(prior_knowledge), m_window(window), m_bayes_prior(information_helper_type::value())
            {
            }

            bool has_reset() const
            {
                return this->m_has_reset;
            }

            void reset()
            {
                this->m_current_statistic = 0.0;
                this->m_window.reset();
                this->m_has_reset = false;
            }

            double observe(const std::array<double, count_streams>& instantaneous_loglikelihood)
            {
                this->m_current_statistic = 0.0;
                this->m_window.observe(instantaneous_loglikelihood);

                std::size_t sz = this->m_window.get_depth();
                this->m_has_reset = sz == 0; // TODO: implement as <window_type> property.

                for (std::size_t s = 0; s < sz; s++)
                {
                    std::array<double, count_streams> local_stat;
                    for (std::size_t k = 0; k < count_streams; k++)
                    {
                        local_stat[k] = type::execute(this->m_window.get_history_at(k, s), this->m_bayes_prior);
                    }
                    double m = type::fuse(local_stat);
                    if (m > this->m_current_statistic) this->m_current_statistic = m;
                }
                this->m_current_statistic = std::log(this->m_current_statistic);
                return this->m_current_statistic;
            }

            // Returns the complete loglikelihood ratio.
            const std::array<double, count_streams>& complete() const
            {
                return this->m_window.complete();
            }

            // Returns the accumulated loglikelihood ratio.
            const std::array<double, count_streams>& accumulated() const
            {
                return this->m_window.accumulated();
            }

            friend std::ostream& operator <<(std::ostream& os, const type& that)
            {
                return os << "bayes_cusum_" << that.m_prior_knowledge << "_" << that.m_window;
            }
        };
    }
}

#endif // ROPUFU_MULTI_SENSOR_STATISTIC_BAYES_CUSUM_HPP_INCLUDED
